---
title: Web Worker 架构
---

一些模块，例如公式引擎，天生是比较消耗资源的，为了避免阻塞主线程从而影响交互体验，Univer 支持将这些模块运行在 Web Worker 当中。

运行在 Web Worker 当中的 Univer 代码实际上也是一个 `Univer` 实例，两个 Univer 实例之间通过 `@univerjs/rpc` 包提供的 RPC 机制进行通信，`@univerjs/rpc` 插件还提供了在两个 Univer 实例之间同步数据和 mutation 的机制。

## 例子

在 Univer 当中使用 Web Worker 并使用基于 Web Worker 的公式计算非常简单。

在主线程的 Univer 实例中引入 `@univerjs/rpc` 插件，指定要加载的 Web Worker 入口文件的路径，并将主线程中的公式插件配置为无需计算公式：

```typescript
import type { IUniverRPCMainThreadConfig } from '@univerjs/rpc'
import { UniverRPCMainThread } from '@univerjs/rpc'
import { UniverSheetsFormula } from '@univerjs/sheets-formula'

// ...

univer.registerPlugin(UniverSheetsFormula, {
  notExecuteFormula: true,
})

univer.registerPlugin(UniverRPCMainThread, {
  workerURL: './worker.js',
  unsyncMutations: new Set([RichTextEditingMutation.id]),
})
```

在 Web Worker 的入口文件中实例化另一个 Univer 实例，并注册需要的插件：

```typescript
import { LocaleType, Univer } from '@univerjs/core'
import { UniverFormulaEngine } from '@univerjs/engine-formula'
import { UniverRPCWorkerThreadPlugin } from '@univerjs/rpc'
import { UniverSheets } from '@univerjs/sheets'
import { UniverSheetsFormula } from '@univerjs/sheets-formula'

const univer = new Univer()

univer.registerPlugin(UniverRPCWorkerThreadPlugin)
univer.registerPlugin(UniverSheets)
univer.registerPlugin(UniverFormulaEngine)
univer.registerPlugin(UniverSheetsFormula)
```

运行之后，就会看到 Web Worker 线程，并且公式计算会在其中进行。

<Callout>
  可以看到因为在 Web Worker 中无需关心 UI 和渲染，所以引入的 plugin 比主线程中少很多，这就是插件化架构带来的好处之一：在任何环境中 Univer 都能做到以最小的资源运行。
</Callout>

详细内容请参考 [examples/sheets](https://github.com/dream-num/univer/tree/dev/examples/src/sheets)。

## 整体架构

Univer 的 Web Worker 架构图下图所示：

![Web Worker 架构](./web-worker/web-worker-architecture.png)

主线程中的创建的文档和发生的编辑会被同步到 Web Worker：

1. 主线程中的 Univer 实例在文档创建时，位于主线程的 `DataSyncPrimaryController` 会监听到该事件，然后调用 Web Worker 中的 `IRemoteInstanceService` 的 `createInstance` 方法创建一个同样的文档实例。
2. 主线程中的 Univer 实例在执行 mutation 时，位于主线程的 `DataSyncPrimaryController` 会监听到该事件，然后调用 Web Worker 中的 `IRemoteInstanceService` 的 `syncMutation` 方法将该 mutation 应用到 Web Worker 中的 Univer 实例。

Web Worker 的公式计算结果如何写回主线程：

1. Web Worker 中的 Univer 实例在公式计算完毕时会生成一个 mutation 并引用，位于 Web Worker 的 `DataSyncReplicaController` 会监听到该事件，然后调用主线程中的 `IRemoteSyncService` 的 `syncMutation` 方法将该 mutation 应用到主线程中的 Univer 实例。

## RPC 机制

从上面的叙述中，可以看到位于不同线程中的 Univer 模块似乎可以直接调用对方的方法，这是因为 `@univerjs/rpc` 插件提供了 RPC 机制。

以主线程调用 Web Worker 线程中 `IRemoteInstanceService` 为例。在作为客户端的主线程中，需要声明一个 `IRemoteInstanceService` 接口作为依赖，而这个依赖的具体实现，则是 RPC 模块提供的 `toModule` 方法，该方法接受一个 `Channel` 作为参数，返回一个实现了 `IRemoteInstanceService` 接口的对象，该对象实际上是一个 `Proxy`，对该对象的方法的调用会被 `Proxy` 拦截，channel 名称，方法名称和参数会被 RPC 模块序列化之后发送到 Web Worker 线程。

```typescript
export class UniverRPCMainThread extends Plugin {
  override async onStarting(): Promise<void> {
    const worker = new Worker(this._config.workerURL)
    const messageProtocol = createWebWorkerMessagePortOnMain(worker)
    const client = new ChannelClient(messageProtocol)
    const server = new ChannelServer(messageProtocol)

    const dependencies: Dependency[] = [
      [IRemoteInstanceService, {
        useFactory: () => toModule<IRemoteInstanceService>(client.getChannel(RemoteInstanceServiceName)),
      }],
    ]
    dependencies.forEach(dependency => injector.add(dependency))
  }
}
```

在 Web Worker 线程中，需要实现 `IRemoteInstanceService` 接口，然后将其注册到 `ChannelServer` 中，这样 Web Worker 线程的 RPC 模块就可以将主线程中的 RPC 调用转发到 Web Worker 线程中的 `IRemoteInstanceService` 实现。

```typescript
/**
 * This plugin is used to register the RPC services on the worker thread.
 */
export class UniverRPCWorkerThreadPlugin extends Plugin {
  constructor(
    _config: unknown,
    @Inject(Injector) protected override _injector: Injector,
  ) {
  }

  override onStarting(): void {
    const messageProtocol = createWebWorkerMessagePortOnWorker()
    const server = new ChannelServer(messageProtocol)

    const dependencies: Dependency[] = [
      [IRemoteInstanceService, { useClass: RemoteInstanceReplicaService }],
    ]
    dependencies.forEach(dependency => this._injector.add(dependency))
    server.registerChannel(RemoteInstanceServiceName, fromModule(this._injector.get(IRemoteInstanceService)))
  }
}
```

实际上，`@univerjs/rpc` 插件提供的 RPC 机制可以用于位于任意两个环境中的 Univer 实例之间的通信，不仅限于主线程和 Web Worker，所以实现服务端计算或者是基于 Electron 的多进程也是简单易行的，只需要实现对应环境中的通信机制（即 `messageProtocol`）即可。
